
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Media;
using Xunit;
using Xunit.Extensions;


namespace Boilen.Primitives {

    public class TestTypeRepository {

        [Fact]
        public void UsedNamespaces_default_is_empty( ) {
            var tr = new TypeRepository( );

            Assert.Empty( tr.UsedNamespaces );
        }


        [Fact]
        public void AddNamespace_fails_for_null_namespace( ) {
            string nullNamespace = null;
            var tr = new TypeRepository( );

            Assert.Throws<ArgumentNullException>( ( ) => tr.AddNamespace( nullNamespace ) );
        }

        [Fact]
        public void AddNamespace_does_not_add_empty_namespace( ) {
            string emptyNamespace = "";
            var tr = new TypeRepository( );

            tr.AddNamespace( emptyNamespace );

            Assert.DoesNotContain( emptyNamespace, tr.UsedNamespaces );
        }

        [Fact]
        public void AddNamespace_adds_namespace_to_UsedNamespaces( ) {
            string expectedNamespace = "TestNamespace";
            var tr = new TypeRepository( );

            tr.AddNamespace( expectedNamespace );

            Assert.Contains( expectedNamespace, tr.UsedNamespaces );
        }


        [Fact]
        public void GetTypeNamespace_fails_for_null_type( ) {
            Type type = null;
            var tr = new TypeRepository( );

            Assert.Throws<ArgumentNullException>( ( ) => tr.GetTypeNamespace( type ) );
        }

        [Theory]
        [PropertyData( "TypesWithNames" )]
        public void GetTypeNamespace_succeeds_for_valid_types( Type type, string expectedName ) {
            var tr = new TypeRepository( );

            string nmspace = tr.GetTypeNamespace( type );

            Assert.Equal( type.Namespace, nmspace );
        }

        [Theory]
        [PropertyData( "TemplateTypes" )]
        public void GetTypeNamespace_succeeds_for_TemplateType( Type type, string typeName, string expectedNamespace ) {
            var tr = new TypeRepository( );

            string nmspace = tr.GetTypeNamespace( type );

            Assert.Equal( expectedNamespace, nmspace );
        }

        [Fact]
        public void GetTypeNamespace_succeeds_for_AliasType( ) {
            Type type = typeof( TypeAlias );
            string expectedNamespace = string.Format( "{0} = {1}", type.Name, AliasedType.FullName );
            var tr = new TypeRepository( );

            string nmspace = tr.GetTypeNamespace( type );

            Assert.Equal( expectedNamespace, nmspace );
        }


        [Fact]
        public void GetTypeName_fails_for_null_type( ) {
            Type type = null;
            var tr = new TypeRepository( );

            Assert.Throws<ArgumentNullException>( ( ) => tr.GetTypeName( type ) );
        }

        [Theory]
        [PropertyData( "TypesWithNames" )]
        public void GetTypeName_succeeds_for_valid_types( Type type, string expectedName ) {
            var tr = new TypeRepository( );

            string name = tr.GetTypeName( type );

            Assert.Equal( expectedName, name );
        }

        [Theory]
        [PropertyData( "TypesWithNames" )]
        public void GetTypeName_adds_type_namespace_to_UsedNamespaces( Type type, string expectedName ) {
            string[] expectedNamespaces = GetExpectedNamespaces( type );
            var tr = new TypeRepository( );

            foreach( string expectedNamespace in expectedNamespaces )
                Assert.DoesNotContain( expectedNamespace, tr.UsedNamespaces );

            tr.GetTypeName( type );

            foreach( string expectedNamespace in expectedNamespaces )
                Assert.Contains( expectedNamespace, tr.UsedNamespaces );
        }

        [Theory]
        [PropertyData( "TypesWithNames" )]
        public void GetTypeName_adds_type_namespace_to_UsedNamespaces_only_once( Type type, string expectedName ) {
            string typeNamespace = type.Namespace;
            var tr = new TypeRepository( );

            tr.GetTypeName( type );
            tr.GetTypeName( type );
            tr.GetTypeName( type );

            int typeNamespaceCount = tr.UsedNamespaces.Where( s => s == typeNamespace ).Count( );
            Assert.Equal( 1, typeNamespaceCount );
        }

        [Theory]
        [PropertyData( "TemplateTypes" )]
        public void GetTypeName_succeeds_for_TemplateType( Type type, string expectedName, string typeNamespace ) {
            var tr = new TypeRepository( );

            string name = tr.GetTypeName( type );

            Assert.Equal( expectedName, name );
        }

        [Theory]
        [PropertyData( "TemplateTypes" )]
        public void GetTypeName_adds_valid_TemplateType_namespace_to_UsedNamespaces( Type type, string typeName, string typeNamespace ) {
            bool isEmptyNamespace = string.IsNullOrEmpty( typeNamespace );
            var tr = new TypeRepository( );

            Assert.DoesNotContain( typeNamespace, tr.UsedNamespaces );

            tr.GetTypeName( type );

            if( isEmptyNamespace )
                Assert.DoesNotContain( typeNamespace, tr.UsedNamespaces );
            else
                Assert.Contains( typeNamespace, tr.UsedNamespaces );
            Assert.DoesNotContain( type.Namespace, tr.UsedNamespaces );
        }

        [Fact]
        public void GetTypeName_succeeds_for_AliasType( ) {
            Type type = typeof( TypeAlias );
            string expectedName = type.Name;
            var tr = new TypeRepository( );

            string name = tr.GetTypeName( type );

            Assert.Equal( expectedName, name );
        }

        [Fact]
        public void GetTypeName_adds_valid_AliasType_namespace_to_UsedNamespaces( ) {
            Type type = typeof( TypeAlias );
            string expectedNamespace = string.Format( "{0} = {1}", type.Name, AliasedType.FullName );
            var tr = new TypeRepository( );

            string name = tr.GetTypeName( type );

            Assert.Contains( expectedNamespace, tr.UsedNamespaces );
        }


        [Fact]
        public void AddGlobalAlias_fails_for_null_type( ) {
            Type type = null;
            string alias = "alias";

            Assert.Throws<ArgumentNullException>( ( ) => TypeRepository.AddGlobalAlias( type, alias ) );
        }

        [Fact]
        public void AddGlobalAlias_fails_for_null_alias( ) {
            Type type = typeof( UIntPtr );
            string alias = null;

            Assert.Throws<ArgumentNullException>( ( ) => TypeRepository.AddGlobalAlias( type, alias ) );
        }

        [Fact]
        public void AddGlobalAlias_fails_for_empty_alias( ) {
            Type type = typeof( UIntPtr );
            string alias = "";

            Assert.Throws<ArgumentException>( ( ) => TypeRepository.AddGlobalAlias( type, alias ) );
        }

        [Fact]
        public void GetTypeName_returns_AddGlobalAlias_value( ) {
            Type type = typeof( UIntPtr );
            string alias = "pointer";
            string expected = alias;
            var tr = new TypeRepository( );

            using( TypeRepository.AddGlobalAlias( type, alias ) ) {
                string name = tr.GetTypeName( type );

                Assert.Equal( expected, name );
            }
        }

        [Fact]
        public void GetTypeNamespace_returns_AddGlobalAlias_value( ) {
            Type type = typeof( UIntPtr );
            string alias = "pointer";
            string expected = "pointer = System.UIntPtr";
            var tr = new TypeRepository( );

            using( TypeRepository.AddGlobalAlias( type, alias ) ) {
                string name = tr.GetTypeNamespace( type );

                Assert.Equal( expected, name );
            }
        }

        [Fact]
        public void GetTypeName_after_AddGlobalAlias_disposed_returns_type_name( ) {
            Type type = typeof( UIntPtr );
            string alias = "pointer";
            string expected = type.Name;
            var tr = new TypeRepository( );

            using( TypeRepository.AddGlobalAlias( type, alias ) ) { }
            string name = tr.GetTypeName( type );

            Assert.Equal( expected, name );
        }

        [Fact]
        public void GetTypeNamespace_after_AddGlobalAlias_disposed_returns_type_namespace( ) {
            Type type = typeof( UIntPtr );
            string alias = "pointer";
            string expected = type.Namespace;
            var tr = new TypeRepository( );

            using( TypeRepository.AddGlobalAlias( type, alias ) ) { }
            string name = tr.GetTypeNamespace( type );

            Assert.Equal( expected, name );
        }


        [Fact]
        public void GetSimpleTypeName_fails_for_null_type( ) {
            Type type = null;
            var tr = new TypeRepository( );

            Assert.Throws<ArgumentNullException>( ( ) => tr.GetSimpleTypeName( type ) );
        }

        [Theory]
        [PropertyData( "TypesWithNames" )]
        public void GetSimpleTypeName_succeeds_for_valid_types( Type type, string typeName ) {
            int arrayNameIndex = typeName.IndexOf( '[' );
            int genericNameIndex = typeName.IndexOf( '<' );
            int simplifierIndex =
                arrayNameIndex < 0 ? genericNameIndex :
                genericNameIndex < 0 ? arrayNameIndex :
                Math.Min( arrayNameIndex, genericNameIndex );
            string expectedName = simplifierIndex < 0 ? typeName : typeName.Substring( 0, simplifierIndex );
            var tr = new TypeRepository( );

            string name = tr.GetSimpleTypeName( type );

            Assert.Equal( expectedName, name );
        }

        [Theory]
        [PropertyData( "TypesWithNames" )]
        public void GetSimpleTypeName_does_not_add_type_namespace_to_UsedNamespaces( Type type, string expectedName ) {
            string typeNamespace = type.Namespace;
            var tr = new TypeRepository( );

            Assert.DoesNotContain( typeNamespace, tr.UsedNamespaces );

            tr.GetSimpleTypeName( type );

            Assert.DoesNotContain( typeNamespace, tr.UsedNamespaces );
        }


        [Theory]
        [InlineData( true )]
        [InlineData( false )]
        public void GetValueString_returns_expected_value_for_null( bool silverlight ) {
            string expectedValue = "null";
            object nullValue = null;
            var tr = new TypeRepository( );

            string stringValue = tr.GetValueString( nullValue, silverlight );

            Assert.Empty( tr.UsedNamespaces );
            Assert.Equal( expectedValue, stringValue );
        }

        [Theory]
        [InlineData( true, "" )]
        [InlineData( true, "a string" )]
        [InlineData( false, "" )]
        [InlineData( false, "a string" )]
        public void GetValueString_returns_expected_value_for_strings( bool silverlight, string value ) {
            string expectedValue = value.Length == 0 ? "string.Empty" : '"' + value + '"';
            var tr = new TypeRepository( );

            string stringValue = tr.GetValueString( value, silverlight );

            Assert.Empty( tr.UsedNamespaces );
            Assert.Equal( expectedValue, stringValue );
        }

        [Theory]
        [InlineData( true, null )]
        [InlineData( true, "Blue" )]
        [InlineData( false, null )]
        [InlineData( false, "Blue" )]
        public void GetValueString_returns_expected_value_for_brushes( bool silverlight, string name ) {
            Color color = name == null
                ? DefaultColor
                : (Color)typeof( Colors ).GetProperty( name, BindingFlags.Public | BindingFlags.Static ).GetValue( null, null );
            Brush value = new SolidColorBrush( color );

            string expectedValue;
            if( name == null )
                expectedValue = string.Format( "new SolidColorBrush(Color.FromArgb(0x{0:x}, 0x{1:x}, 0x{2:x}, 0x{3:x}))", color.A, color.R, color.G, color.B );
            else if( silverlight )
                expectedValue = "new SolidColorBrush(Colors." + name + ")";
            else
                expectedValue = "Brushes." + name;
            var tr = new TypeRepository( );

            string stringValue = tr.GetValueString( value, silverlight );

            Assert.Equal( expectedValue, stringValue );
        }

        [Theory]
        [InlineData( true, FrameworkPropertyMetadataOptions.AffectsArrange )]
        [InlineData( true, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange )]
        [InlineData( false, FrameworkPropertyMetadataOptions.AffectsArrange )]
        [InlineData( false, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange )]
        public void GetValueString_returns_expected_value_for_enums( bool silverlight, FrameworkPropertyMetadataOptions value ) {
            Type enumValueType = value.GetType( );
            string nmspace = enumValueType.Namespace;
            string typeNamePrefix = enumValueType.Name + '.';
            string expectedValue = typeNamePrefix + value.ToString( ).Replace( ", ", " | " + typeNamePrefix );
            var tr = new TypeRepository( );

            string stringValue = tr.GetValueString<FrameworkPropertyMetadataOptions>( value, silverlight );

            Assert.Equal( expectedValue, stringValue );
            Assert.Contains( nmspace, tr.UsedNamespaces );
        }

        [Theory]
        [PropertyData( "SimpleValues" )]
        public void GetValueString_returns_expected_value_for_simple_values( object value, string expectedValue ) {
            var tr = new TypeRepository( );

            string stringValue = tr.GetValueString( value, false );

            Assert.Empty( tr.UsedNamespaces );
            Assert.Equal( expectedValue, stringValue );
        }


        #region Utility

        private static readonly Color DefaultColor = Color.FromArgb( 0xFF, 0x12, 0x34, 0x56 );

        public static IEnumerable<object[]> TypesWithNames {
            get {
                yield return new object[] { typeof( int ), "int" };
                yield return new object[] { typeof( double ), "double" };
                yield return new object[] { typeof( object ), "object" };
                yield return new object[] { typeof( void ), "void" };
                yield return new object[] { typeof( int[] ), "int[]" };
                yield return new object[] { typeof( double[] ), "double[]" };
                yield return new object[] { typeof( object[] ), "object[]" };
                yield return new object[] { typeof( object[,] ), "object[,]" };
                yield return new object[] { typeof( object[, ,] ), "object[,,]" };
                yield return new object[] { typeof( object[][] ), "object[][]" };
                yield return new object[] { typeof( object[][][] ), "object[][][]" };

                var simpleTypes = new[] { typeof( Exception ), typeof( Array ), typeof( EventArgs ) };
                foreach( Type simpleType in simpleTypes ) {
                    yield return new object[] { simpleType, simpleType.Name };
                    yield return new object[] { simpleType.MakeArrayType( ), simpleType.Name + "[]" };
                }

                var genericTypes = new[] { typeof( Nullable<> ), typeof( List<Exception> ), typeof( Dictionary<Exception, Exception> ) };
                foreach( Type genericType in genericTypes ) {
                    string genericName = genericType.Name;
                    genericName = genericName.Substring( 0, genericName.IndexOf( '`' ) )
                        + "<"
                        + genericType.GetGenericArguments( ).Aggregate( "", ( s, t ) => s + ", " + t.Name, s => s.Substring( 2 ) )
                        + ">";
                    yield return new object[] { genericType, genericName };
                    yield return new object[] { genericType.MakeArrayType( ), genericName + "[]" };
                }

                yield return new object[] { typeof( Dictionary<int, List<double>> ), "Dictionary<int, List<double>>" };
                yield return new object[] { typeof( Dictionary<int, List<double>>[] ), "Dictionary<int, List<double>>[]" };
                yield return new object[] { typeof( Dictionary<int, List<double>>[][] ), "Dictionary<int, List<double>>[][]" };
                yield return new object[] { typeof( Dictionary<int, System.Collections.ArrayList[]>[] ), "Dictionary<int, ArrayList[]>[]" };
            }
        }

        public static IEnumerable<object[]> SimpleValues {
            get {
                yield return new object[] { 0, "0" };
                yield return new object[] { -12, "-12" };
                yield return new object[] { 1.1, "1.1" };
                yield return new object[] { Size.Empty, "Size.Empty" };
                yield return new object[] { new Size( 1.2, 3.4 ), "new Size(1.2, 3.4)" };
                yield return new object[] { new Point( 1.2, 3.4 ), "new Point(1.2, 3.4)" };
                yield return new object[] { new Thickness( 1.2 ), "new Thickness(1.2)" };
                yield return new object[] { new Thickness( 1.2, 3.4, double.NaN, double.PositiveInfinity ), "new Thickness(1.2, 3.4, double.NaN, double.PositiveInfinity)" };
            }
        }


        private static string[] GetExpectedNamespaces( Type type ) {
            return GetExpectedNamespacesCore( type )
                .Distinct( )
                .ToArray( );
        }

        private static IEnumerable<string> GetExpectedNamespacesCore( Type type ) {
            if( !string.IsNullOrEmpty( type.Namespace ) )
                yield return type.Namespace;

            if( type.IsGenericType )
                foreach( string ns in type.GetGenericArguments( ).SelectMany( GetExpectedNamespacesCore ) )
                    yield return ns;

            if( type.IsArray )
                foreach( string ns in GetExpectedNamespacesCore( type.GetElementType( ) ) )
                    yield return ns;
        }


        [TemplateType( )]
        private class TemplateTypeWithoutNamespace { }

        [TemplateType( Namespace = "Type.Namespace" )]
        private class TemplateTypeWithNamespace { }

        public static IEnumerable<object[]> TemplateTypes {
            get {
                Type[] templateTypes = new[] { typeof( TemplateTypeWithoutNamespace ), typeof( TemplateTypeWithNamespace ) };
                string[] templateTypeNamespaces = new[] { "", "Type.Namespace" };

                for( int i = 0; i < templateTypes.Length; ++i ) {
                    Type templateType = templateTypes[i];
                    string templateTypeName = templateType.Name;
                    string templateTypeNamespace = templateTypeNamespaces[i];

                    yield return new object[] { templateType, templateTypeName, templateTypeNamespace };
                    yield return new object[] { templateType.MakeArrayType( ), templateTypeName + "[]", templateTypeNamespace };
                }
            }
        }


        private static readonly Type AliasedType = typeof( string );

        [AliasType( typeof( string ) )]
        private class TypeAlias { }

        #endregion

    }

}
